// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <limits.h>
#include <stdlib.h>

#include <hypervisor/guest.h>
#include <pretty/hexdump.h>
#include <unittest/unittest.h>

static void* page_addr(void* base, size_t page) {
    uintptr_t addr = reinterpret_cast<uintptr_t>(base);
    addr += PAGE_SIZE * page;
    return reinterpret_cast<void*>(addr);
}

static void hexdump_result(void* actual, void* expected) {
    printf("\nactual:\n");
    hexdump_ex(page_addr(actual, 0), 16, PAGE_SIZE * 0);
    hexdump_ex(page_addr(actual, 1), 16, PAGE_SIZE * 1);
    hexdump_ex(page_addr(actual, 2), 16, PAGE_SIZE * 2);
    hexdump_ex(page_addr(actual, 3), 32, PAGE_SIZE * 3);
    printf("expected:\n");
    hexdump_ex(page_addr(expected, 0), 16, PAGE_SIZE * 0);
    hexdump_ex(page_addr(expected, 1), 16, PAGE_SIZE * 1);
    hexdump_ex(page_addr(expected, 2), 16, PAGE_SIZE * 2);
    hexdump_ex(page_addr(expected, 3), 32, PAGE_SIZE * 3);
}

#define ASSERT_EPT_EQ(actual, expected, size, ...)                                                 \
    do {                                                                                           \
        int cmp = memcmp(actual, expected, size);                                                  \
        if (cmp != 0)                                                                              \
            hexdump_result(actual, expected);                                                      \
        ASSERT_EQ(cmp, 0, ##__VA_ARGS__);                                                          \
    } while (0)

#define INITIALIZE_PAGE_TABLE                                                                      \
    {                                                                                              \
        {                                                                                          \
            { 0 }                                                                                  \
        }                                                                                          \
    }

typedef struct {
    uint64_t entries[512];
} page_table;

enum {
    X86_PTE_P = 0x01,  /* P    Valid           */
    X86_PTE_RW = 0x02, /* R/W  Read/Write      */
    X86_PTE_PS = 0x80, /* PS   Page size       */
};

static bool page_table_1gb(void) {
    BEGIN_TEST;

    uintptr_t pte_off;
    page_table actual[4] = INITIALIZE_PAGE_TABLE;
    page_table expected[4] = INITIALIZE_PAGE_TABLE;

    ASSERT_EQ(guest_create_page_table((uintptr_t)actual, 1 << 30, &pte_off), ZX_OK);

    // pml4
    expected[0].entries[0] = PAGE_SIZE | X86_PTE_P | X86_PTE_RW;
    // pdp
    expected[1].entries[0] = X86_PTE_P | X86_PTE_RW | X86_PTE_PS;
    ASSERT_EPT_EQ(actual, expected, sizeof(actual));
    ASSERT_EQ(pte_off, PAGE_SIZE * 2u);

    END_TEST;
}

static bool page_table_2mb(void) {
    BEGIN_TEST;

    uintptr_t pte_off;
    page_table actual[4] = INITIALIZE_PAGE_TABLE;
    page_table expected[4] = INITIALIZE_PAGE_TABLE;

    ASSERT_EQ(guest_create_page_table((uintptr_t)actual, 2 << 20, &pte_off), ZX_OK);

    // pml4
    expected[0].entries[0] = PAGE_SIZE | X86_PTE_P | X86_PTE_RW;
    // pdp
    expected[1].entries[0] = PAGE_SIZE * 2 | X86_PTE_P | X86_PTE_RW;
    // pd
    expected[2].entries[0] = X86_PTE_P | X86_PTE_RW | X86_PTE_PS;
    ASSERT_EPT_EQ(actual, expected, sizeof(actual));
    ASSERT_EQ(pte_off, PAGE_SIZE * 3u);

    END_TEST;
}

static bool page_table_4kb(void) {
    BEGIN_TEST;

    uintptr_t pte_off;
    page_table actual[4] = INITIALIZE_PAGE_TABLE;
    page_table expected[4] = INITIALIZE_PAGE_TABLE;

    ASSERT_EQ(guest_create_page_table((uintptr_t)actual, 4 * 4 << 10, &pte_off), ZX_OK);

    // pml4
    expected[0].entries[0] = PAGE_SIZE | X86_PTE_P | X86_PTE_RW;
    // pdp
    expected[1].entries[0] = PAGE_SIZE * 2 | X86_PTE_P | X86_PTE_RW;
    // pd
    expected[2].entries[0] = PAGE_SIZE * 3 | X86_PTE_P | X86_PTE_RW;
    // pt
    expected[3].entries[0] = PAGE_SIZE * 0 | X86_PTE_P | X86_PTE_RW;
    expected[3].entries[1] = PAGE_SIZE * 1 | X86_PTE_P | X86_PTE_RW;
    expected[3].entries[2] = PAGE_SIZE * 2 | X86_PTE_P | X86_PTE_RW;
    expected[3].entries[3] = PAGE_SIZE * 3 | X86_PTE_P | X86_PTE_RW;
    ASSERT_EPT_EQ(actual, expected, sizeof(actual));
    ASSERT_EQ(pte_off, PAGE_SIZE * 4u);

    END_TEST;
}

static bool page_table_mixed_pages(void) {
    BEGIN_TEST;

    uintptr_t pte_off;
    page_table actual[4] = INITIALIZE_PAGE_TABLE;
    page_table expected[4] = INITIALIZE_PAGE_TABLE;

    ASSERT_EQ(guest_create_page_table((uintptr_t)actual, (2 << 20) + (4 << 10), &pte_off), ZX_OK);

    // pml4
    expected[0].entries[0] = PAGE_SIZE | X86_PTE_P | X86_PTE_RW;
    // pdp
    expected[1].entries[0] = PAGE_SIZE * 2 | X86_PTE_P | X86_PTE_RW;

    // pd
    expected[2].entries[0] = X86_PTE_P | X86_PTE_RW | X86_PTE_PS;
    expected[2].entries[1] = PAGE_SIZE * 3 | X86_PTE_P | X86_PTE_RW;

    // pt
    expected[3].entries[0] = (2 << 20) | X86_PTE_P | X86_PTE_RW;
    ASSERT_EPT_EQ(actual, expected, sizeof(actual));
    ASSERT_EQ(pte_off, PAGE_SIZE * 4u);

    END_TEST;
}

// Create a page table for 2gb + 123mb + 32kb bytes.
static bool page_table_complex(void) {
    BEGIN_TEST;

    uintptr_t pte_off;
    page_table actual[4] = INITIALIZE_PAGE_TABLE;
    page_table expected[4] = INITIALIZE_PAGE_TABLE;

    // 2gb + 123mb + 32kb of RAM. This breaks down as follows:
    //
    // PML4
    // > 1 pointer to a PDPT
    //
    // PDPT
    // > 2 direct-mapped 1gb regions
    // > 1 ponter to a PD
    //
    // PD
    // > 61 direct-mapped 2mb regions
    // > 1 pointer to a PT
    //
    // PT
    // >  264 mapped pages
    ASSERT_EQ(guest_create_page_table((uintptr_t)actual, 0x87B08000, &pte_off), ZX_OK);

    // pml4
    expected[0].entries[0] = PAGE_SIZE | X86_PTE_P | X86_PTE_RW;

    // pdp
    expected[1].entries[0] = (0l << 30) | X86_PTE_P | X86_PTE_RW | X86_PTE_PS;
    expected[1].entries[1] = (1l << 30) | X86_PTE_P | X86_PTE_RW | X86_PTE_PS;
    expected[1].entries[2] = PAGE_SIZE * 2 | X86_PTE_P | X86_PTE_RW;

    // pd - starts at 2GB
    const uint64_t pdp_offset = 2l << 30;
    for (int i = 0; i < 62; ++i) {
        expected[2].entries[i] = (pdp_offset + (i << 21)) | X86_PTE_P | X86_PTE_RW | X86_PTE_PS;
    }
    expected[2].entries[61] = PAGE_SIZE * 3 | X86_PTE_P | X86_PTE_RW;

    // pt - starts at 2GB + 122MB
    const uint64_t pd_offset = pdp_offset + (61l << 21);
    for (int i = 0; i < 264; ++i) {
        expected[3].entries[i] = (pd_offset + (i << 12)) | X86_PTE_P | X86_PTE_RW;
    }
    ASSERT_EPT_EQ(actual, expected, sizeof(actual));
    ASSERT_EQ(pte_off, PAGE_SIZE * 4u);

    END_TEST;
}

BEGIN_TEST_CASE(extended_page_table)
RUN_TEST(page_table_1gb)
RUN_TEST(page_table_2mb)
RUN_TEST(page_table_4kb)
RUN_TEST(page_table_mixed_pages)
RUN_TEST(page_table_complex)
END_TEST_CASE(extended_page_table)
